package com.shuke.medical.insure.remote;

import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.net.NetUtil;
import cn.hutool.crypto.SecureUtil;
import com.alibaba.fastjson.JSONObject;
import org.apache.commons.lang.StringUtils;
import com.shuke.medical.insure.config.ChsProperties;
import com.shuke.medical.insure.eums.ChsInfNoEnum;
import com.shuke.medical.insure.eums.InfCodeEnum;
import com.shuke.medical.insure.model.bo.ChsSignNoBO;
import com.shuke.medical.insure.remote.body.ChsRequestBody;
import com.shuke.medical.insure.remote.body.ChsResponseBody;
import com.shuke.medical.insure.remote.consts.ChsCacheConst;
import com.shuke.medical.insure.utils.ChsInOutUtil;
import com.shuke.medical.insure.utils.SM3Utils;
import com.shuke.medical.insure.utils.SignUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;
import org.apache.http.HttpEntity;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetAddress;
import java.util.Arrays;
import java.util.Date;
import java.util.concurrent.TimeUnit;


/***
 * @title HttpRequestComponent
 * @description
 * @author woo hua
 * @version 1.0.0
 * @create 2023/3/23 10:49
 **/
@Slf4j
@Component
public class ChsRemoteHttp {
    /**
     * 向服务端请求超时时间设置(单位:毫秒)
     */
    private static int SERVER_REQUEST_TIME_OUT = 5000;
    /**
     * 服务端响应超时时间设置(单位:毫秒)
     */
    private static int SERVER_RESPONSE_TIME_OUT = 5000;
    /**
     * 从连接池获取连接的超时时间
     */
    private static int CONNECTION_REQUEST_TIME_OUT = 5000;
    /**
     * 登录失败重试次数
     */
    private static final int RETRY_SIGN_NO = 2;

    private static final RequestConfig requestConfig = RequestConfig.custom()
            .setSocketTimeout(SERVER_REQUEST_TIME_OUT)
            .setConnectTimeout(SERVER_RESPONSE_TIME_OUT)
            .setConnectionRequestTimeout(CONNECTION_REQUEST_TIME_OUT)
            .build();

    @Resource
    private ChsProperties chsProperties;
    @Resource
    private RedisTemplate redisTemplate;

    /**
     * 签到流水号:通过签到【9001】交易获取
     * 对于定点医疗机构，在调用API交易之前，必须首先进行一次签到登录。
     * 签到登录交易调用后，将得到一个SIGN_NO，首次登陆后24小时内有效，超过24小时内失效。
     * 因此每个定点医疗机构需要每天至少登录一次，获得最新的交易验证码SIGN_NO，每个业务交易都需要附加此交易验证码。
     * 如果SIGN_NO失效，所有API请求将返回错误代码 -9 , 判断请求返回 -9 后，请立即执行登录，获取最新的SIGN_NO。
     *
     * @return
     */
    public String signNo() throws Exception {
        log.info("立即执行登录，获取最新的SIGN_NO...");
        ChsSignNoBO.Input signNoInput = new ChsSignNoBO.Input();
        InetAddress localHost = NetUtil.getLocalhost();
        String ip = StringUtils.isEmpty(chsProperties.getDynamicConfig().getIp()) ? localHost.getHostAddress() : chsProperties.getDynamicConfig().getIp();
        String mac = StringUtils.isEmpty(chsProperties.getDynamicConfig().getMac()) ? NetUtil.getMacAddress(localHost) : chsProperties.getDynamicConfig().getMac();
        signNoInput.setMac(mac);
        signNoInput.setIp(ip);
        signNoInput.setUserid(chsProperties.getAccount().getUserid());
        signNoInput.setOpter_no(chsProperties.getAccount().getOpterNo());
        signNoInput.setCurrenttime(DateUtil.format(new Date(), DatePattern.PURE_DATETIME_PATTERN));
        // 登录时间（14位）、协议医疗机构接入提供的密码，两者拼接后再通过32位的MD5算法加密（字母大写）生成密文，
        // 注意拼接的顺序，时间在前密码在后。例如：登录时间：20190618163926，登录密码：888888，那么password的值为20190618163926888888
        String password = signNoInput.getCurrenttime() + chsProperties.getAccount().getPassword();
        // 该字符串通过32位的MD5算法加密（字母大写）生成密文86B31EDFE60F7BA28765DF860D56B7B2
        password = SecureUtil.md5(password).toUpperCase();
        signNoInput.setPassword(password);


        log.info("核心系统登录签到请求参数 signNoInput: [{}]", signNoInput);

        for (int i = 0; i < RETRY_SIGN_NO; i++) {
            try {
                String result = doPost(ChsInfNoEnum.SIGN_ON, ChsInOutUtil.toInPut(signNoInput));
                // 登录签到成功， 更新签到流水号
                if (StringUtils.isNotEmpty(result)) {
                    log.info("登录签到成功， 更新签到流水号。result: [{}]", result);
                    ChsSignNoBO.Output output = ChsInOutUtil.toOutPut(result, ChsSignNoBO.Output.class);
                    String sign_no = output.getSign_no();
                    redisTemplate.opsForValue().set(ChsCacheConst.CHS_SIGN_NO, sign_no, ChsCacheConst.CHS_SIGN_NO_EXPIRE, TimeUnit.SECONDS);
                    return sign_no;
                }
            } catch (Exception e) {
                log.error("登录签到失败，失败原因 cause: [{}]", e.getMessage());
                log.warn("登录签到失败，已经重试次数: [{}],正在进行重试...", i);
            }
        }
        log.error("登录签到失败，重试{}次后仍然失败，抛异常...", RETRY_SIGN_NO);
        throw new Exception("登录签到，获取最新的SIGN_NO失败");
    }

    public String doPost(ChsInfNoEnum infNo, JSONObject input) throws Exception {
        return doPost(infNo, chsProperties.getAccount().getOpterNo(), chsProperties.getAccount().getOpterName(), input);
    }

    private String doPost(ChsInfNoEnum infNo, String opTer, String opTerName, JSONObject input) throws Exception {
        String url = chsProperties.getDynamicConfig().getUrl()
                .replace("{suffix}", infNo.getSuffix());
        log.info("==============请求医保核心系统 - [{}]，url：[{}]", infNo, url);
        HttpPost httpPost = new HttpPost(url);
        // 构建请求头
        buildRequestHeader(httpPost);
        log.info("==============请求医保核心系统 - [{}]，请求头：[{}]", infNo, Arrays.toString(httpPost.getAllHeaders()));

        // 构建请求体
        ChsRequestBody requestBody = buildRequestBody(infNo, input, opTer, opTerName);
        String json = JSONObject.toJSONString(requestBody);
        log.info("==============请求医保核心系统 - [{}]，请求体：[{}]", infNo, json);
        // 发起 http 请求
        String outPut = postHttp(httpPost, json, infNo);
        return outPut;
    }

    /**
     * 网关响应分为两类，一类是正常响应，一类是异常响应。
     * 正常响应时，网关直接把结果透传给调用者，不做任何操作。
     * 异常响应时，网关会返回{code:xxx,,message:xxx}结构的json数据。
     * Code字段为状态码，message为异常信息。
     * {"code":401,"message":"缺少服务网关的请求头!"}	请求中缺少服务网关所需的请求头。
     * {"code":401,"message":"签名时间戳超时!"}	时间戳超时，默认为30分钟。
     * {"code":401,"message":"非法用户!"}	请求头中携带的AK不是合法的AK
     * {"code":401,"message":"签名不一致!"}	签名不一致,请检查您的签名方式
     *
     * @param httpPost
     * @param json
     * @param infNo
     * @return
     * @throws Exception
     */
    private String postHttp(HttpPost httpPost, String json, ChsInfNoEnum infNo) throws Exception {
        HttpEntity entity = new StringEntity(json, ContentType.APPLICATION_JSON);
        httpPost.setEntity(entity);

        CloseableHttpClient httpClient = HttpClients.custom().setDefaultRequestConfig(requestConfig).build();
        String result;
        long startTime = System.currentTimeMillis();
        try {

            log.debug("==============请求医保核心系统 - [{}]，start Time : {}", infNo, System.currentTimeMillis());
            CloseableHttpResponse httpResponse = httpClient.execute(httpPost);
            log.debug("==============请求医保核心系统 - [{}]，end Time : {} , 耗时 : {}", infNo, System.currentTimeMillis(), (System.currentTimeMillis() - startTime) / 1000);
            entity = httpResponse.getEntity();
            result = IOUtils.toString(entity.getContent());
        } catch (IOException e) {
            log.error("==============请求医保核心系统失败 - [{}]，cause: [{}]", infNo, e.getMessage());
            throw new Exception(e);
        }

        log.info("==============请求医保核心系统 - [{}]，请求返回结果：[{}]", infNo, result);

        JSONObject jsonObject = JSONObject.parseObject(result);
        if (jsonObject.containsKey("code") && jsonObject.containsKey("message")) {
            log.error("请求医保核心系统，网关响应异常。 result: [{}]", jsonObject);
            throw new Exception(String.valueOf(jsonObject.get("message")));
        }
        ChsResponseBody responseBody = JSONObject.parseObject(result, ChsResponseBody.class);

        InfCodeEnum infCodeEnum = InfCodeEnum.getByStatus(responseBody.getInfcode());
        switch (infCodeEnum) {
            case SUCCESS:
                return responseBody.getOutput();
            case FAIL:
                log.error("============== 请求医保核心系统失败 - [{}]，err_msg: [{}]", infNo, responseBody.getErr_msg());
                String no_Sign_Err_msg = "错误原因:请先登录";
                if (responseBody.getErr_msg().contains(no_Sign_Err_msg)) {
                    if (ChsInfNoEnum.SIGN_ON.equals(infNo)) {
                        // 调用登录签到接口，返回未登录 （正常情况，不会出现）
                        log.error("调用医保核心系统登录签到接口，返回：[{}], err_msg：[{}]",
                                InfCodeEnum.NO_LOGIN.getVALUE(), responseBody.getErr_msg());
                        throw new Exception("请求医保核心系统调用登录签到接口失败");
                    } else {
                        // 调用非登录签到接口，返回“错误原因:请先登录”，需要先进行一次签到登录，再调用API
                        log.info("出现“错误原因:请先登录”报错，重新进行登录签到");
                        try {
                            signNo();
                        } catch (Exception e) {
                            log.error("调用医保核心系统登录接口失败");
                            throw new Exception("请求医保核心系统调用登录签到接口失败");
                        }
                        // 递归，登录签到后，重新再调用
                        return postHttp(httpPost, json, infNo);
                    }
                }
                throw new Exception(responseBody.getErr_msg());
            case NO_LOGIN:
                if (ChsInfNoEnum.SIGN_ON.equals(infNo)) {
                    // 调用登录签到接口，返回未登录 （正常情况，不会出现）
                    log.error("调用医保核心系统登录签到接口，返回：[{}], err_msg：[{}]",
                            InfCodeEnum.NO_LOGIN.getVALUE(), responseBody.getErr_msg());
                    throw new Exception("请求医保核心系统调用登录签到接口失败");
                } else {
                    // 调用非登录签到接口，返回“未登录”，需要先进行一次签到登录，再调用API
                    try {
                        signNo();
                    } catch (Exception e) {
                        log.error("调用医保核心系统登录接口失败");
                        throw new Exception("请求医保核心系统调用登录签到接口失败");
                    }
                    // 递归，登录签到后，重新再调用
                    return postHttp(httpPost, json, infNo);
                }
            default:
                log.error("============== 请求医保核心系统失败 - [{}], 未知的交易状态码：[{}], 错误信息：[{}], 提示信息：[{}]",
                        infNo, responseBody.getInfcode(), responseBody.getErr_msg(), responseBody.getWarn_msg());
                throw new Exception(responseBody.getErr_msg());
        }
    }

    /**
     * 下载
     *
     * @param infNo
     * @param input
     * @return
     * @throws Exception
     */
    public InputStream doDownLoad(ChsInfNoEnum infNo, JSONObject input) throws Exception {
        return doDownLoad(infNo, chsProperties.getAccount().getOpterNo(), chsProperties.getAccount().getOpterName(), input);
    }

    private InputStream doDownLoad(ChsInfNoEnum infNo, String opTer, String opTerName, JSONObject input) throws Exception {

        String url = chsProperties.getDynamicConfig().getUrl()
                .replace("{suffix}", infNo.getSuffix());
        log.info("==============请求医保核心系统 - [{}]，url：[{}]", infNo, url);
        HttpPost httpPost = new HttpPost(url);
        // 构建请求头
        buildRequestHeader(httpPost);
        log.info("==============请求医保核心系统 - [{}]，请求头：[{}]", infNo, Arrays.toString(httpPost.getAllHeaders()));

        // 构建请求体
        ChsRequestBody requestBody = buildRequestBody(infNo, input, opTer, opTerName);
        String json = JSONObject.toJSONString(requestBody);
        log.info("==============请求医保核心系统 - [{}]，请求体：[{}]", infNo, json);
        // 发起 http 请求
        return doDownLoad(httpPost, json, infNo);
    }

    private InputStream doDownLoad(HttpPost httpPost, String json, ChsInfNoEnum infNo) throws Exception {
        HttpEntity entity = new StringEntity(json, ContentType.APPLICATION_JSON);
        httpPost.setEntity(entity);

        CloseableHttpClient httpClient = HttpClients.custom().setDefaultRequestConfig(requestConfig).build();
        long startTime = System.currentTimeMillis();
        try {
            log.info("==============请求医保核心系统 - [{}]，start Time : {}", infNo, System.currentTimeMillis());
            CloseableHttpResponse httpResponse = httpClient.execute(httpPost);
            log.info("==============请求医保核心系统 - [{}]，end Time : {} , 耗时 : {}", infNo, System.currentTimeMillis(), (System.currentTimeMillis() - startTime) / 1000);
            entity = httpResponse.getEntity();
            return entity.getContent();
        } catch (IOException e) {
            log.error("==============请求医保核心系统失败 - [{}]，cause: [{}]", infNo, e.getMessage());
            throw new Exception(e);
        }
    }

    private void buildRequestHeader(HttpPost httpPost) {
        httpPost.addHeader("Content-Type", "application/json");
        httpPost.addHeader("Accept", "application/json;charset=UTF-8");

        String passId = chsProperties.getPassId();
        String secretKey = chsProperties.getSecretKey();

        // 非重复的随机字符串（并非定长100位，十分钟内不能重复）。
        String xRioNonce = SignUtil.getRandomString(32);
        // 当前 unix 时间戳（秒）。
        String xRioTimestamp = String.valueOf(SignUtil.getCurrentUnixSeconds());
        // Sha256加密生成的签名。x-tif-signature = sha256(x-tif-timestamp + secretKey+ x-tif-nonce + xtif-timestamp)
        String signature = SignUtil.getSHA256Str(xRioTimestamp + secretKey + xRioNonce + xRioTimestamp);

        httpPost.addHeader("x-tif-paasid", passId);
        httpPost.addHeader("x-tif-signature", signature);
        httpPost.addHeader("x-tif-timestamp", xRioTimestamp);
        httpPost.addHeader("x-tif-nonce", xRioNonce);
    }

    private ChsRequestBody buildRequestBody(ChsInfNoEnum infNo, JSONObject input, String opTer, String opTerName) throws Exception {
        Object signNoValue = redisTemplate.opsForValue().get(ChsCacheConst.CHS_SIGN_NO);
        String signNo = null != signNoValue ? String.valueOf(signNoValue) : "";
        // 跳过第一次签到登录
        if (!ChsInfNoEnum.SIGN_ON.equals(infNo) && StringUtils.isEmpty(signNo)) {
            signNo = signNo();
        }

        return new ChsRequestBody()
                //1.	infno	交易编号	字符型	6		Y	交易编号详见接口列表
                .setInfno(infNo.getValue())
                //2.	msgid	发送方报文ID	字符型	30		Y	定点医药机构编号(12)+时间(14)+ 顺序号(4)
                .setMsgid(chsProperties.getFixmedinsCode() + DateUtil.format(new Date(), DatePattern.PURE_DATETIME_PATTERN) + chsProperties.getSequenceNum())
                //3.	insuplc_admdvs	参保地医保区划	字符型	6			如果参保人个人待遇业务办理时，此项为必填。见医保区划表。
                .setInsuplc_admdvs(chsProperties.getInsuplcAdmdvs())
                //4.	mdtrtarea_admvs	就医地医保区划	字符型	6		Y
                .setMdtrtarea_admvs(chsProperties.getMdtrtareaAdmvs())
                //5.	dev_no	设备编号	字符型	100
                .setDev_no("")
                //6.	dev_safe_info	设备安全信息	字符型	2000、
                .setDev_safe_info("")
                //7.	signtype	签名类型	字符型	10			固定传SM3，示例详见2.9章节    ChsProperties.getSigntype()
                .setSigntype(chsProperties.getSigntype())
                //8.	cainfo	数字签名信息	字符型	1024			对input按签名类型进行签名     SM3Utils.encodeSM3(JsonUtils.toJson(input))
                .setCainfo(SM3Utils.encodeSM3(input.toString()))
                //9.	infver	接口版本号	字符型	6		Y	例如：“V1.0”，版本号由医保下发通知。
                .setInfver(chsProperties.getInfver())
                //10.	opter_type	经办人类别	字符型	3	Y	Y	1-经办人；2-自助终端；3-移动终端
                .setOpter_type(chsProperties.getOpterType())
                //11.	opter	经办人	字符型	30		Y	传入经办人编号
                .setOpter(opTer)
                // 12.	opter_name	经办人姓名	字符型	50		Y
                .setOpter_name(opTerName)
                //13.	inf_time	交易时间	字符型	19		Y	格式：yyyy-MM-dd HH:mm:ss
                .setInf_time(DateUtil.format(new Date(), DatePattern.NORM_DATETIME_PATTERN))
                //14.	fixmedins_code	定点医疗机构编号	字符型	30		Y
                .setFixmedins_code(chsProperties.getFixmedinsCode())
                //15.	sign_no	签到流水号	字符型	100			通过签到【9001】交易获取
                .setSign_no(signNo)
                //16.	input	交易输入	字符型	40000		Y
                .setInput(input)
                //17.	recer_sys_code	接收方系统代码	字符型	10		Y	用于多套系统接入，区分不同系统使用
                .setRecer_sys_code(chsProperties.getRecerSysCode());
    }
}
